β

Как стать автором
Курсы по фронтенду
Посмотри в Календарь, вдруг сегодня есть мероприятие?

Моя лентаВсе потоки

OlegSchwann 25 июн 2018 в 15:13

Законы рефлексии в Gо

9 мин

53K

Go*Программирование*

Из песочницы

Привет, Хабр! Представляю вашему вниманию перевод статьи «The Laws of Reflection» от создателя языка.

Рефлексия — способность программы исследовать собственную структуру, в особенности через типы. Это форма метапрограммирования и отличный источник путаницы.

В Go рефлексия широко используется, например, в пакетах test и fmt. В этой статье попытаемся избавиться от «магии», объяснив, как рефлексия работает в Go.

Типы и Интерфейсы

Так как рефлексия основывается на системе типов, давайте освежим знания о типах в Go.

Go статически типизирован. Каждая переменная имеет один и только один статический тип, зафиксированный во время компиляции: int, float32, *MyType, []byte… Если мы объявляем:

type MyInt int
var i int
var j MyInt

то i имеет тип int и j имеет тип MyInt. Переменные i и j имеют разные статические типы и, хотя они имеют один и тот же базовый тип, они не могут быть присвоены друг другу без преобразования.

Одной из важных категорий типа являются интерфейсы, которые представляют собой фиксированные множества методов. Интерфейс может хранить любое конкретное (неинтерфейсное) значение, пока это значение реализует методы интерфейса. Известной парой примеров является io.Reader и io.Writer, типы Reader и Writer из пакета io:

// Reader - это интерфейс, оборачивающий базовый метод Read().
type Reader interface {
Read(p []byte) (n int, err error)
}
// Writer - это интерфейс, оборачивающий базовый метод Write().
type Writer interface {
Write(p []byte) (n int, err error)
}

Говорят, что любой тип, который реализует метод Read() или Write() с этой сигнатурой, реализует io.Reader или io.Writer соответственно. Это означает, что переменная типа io.Reader может содержать любое значение, тип которого имеет метод Read():

var r io.Reader
r = os.Stdin
r = bufio.NewReader(r)
r = new(bytes.Buffer)

Важно понять, что r может присваиваться любое реализующее io.Reader значение. Go статически типизирован, а статический тип r — io.Reader.

Чрезвычайно важным примером типа интерфейса является пустой интерфейс:

interface{}

Он представляет собой пустое множество ∅ методов и реализуется любым значением.

Некоторые говорят, что интерфейсы Go являются переменными с динамической типизацией, но это заблуждение. Они статически типизированы: переменная с типом интерфейс всегда имеет один и тот же статический тип, и хотя во время выполнения значение, хранящееся в переменной интерфейса, может изменять тип, это значение всегда будет удовлетворять интерфейсу. (Никаких undefined, NaN и прочих ломающих логику программы вещей.)

Это надо понять — отражение и интерфейсы тесно связаны.

Внутреннее представление интерфейса

Russ Cox написал подробное сообщение в блоге о утройстве интерфейса в Go. Не менее хорошая статья есть на Habr'е. Здесь нет необходимости повторять всю историю, основные моменты упомянуты.

Переменная типа интерфейса хранит пару: конкретное значение, присвоенное переменной, и дескриптор типа этого значения. Точнее, значение — базовый элемент данных, который реализует интерфейс, а тип описывает полный тип этого элемента. Например, после

var r io.Reader
tty, err := os.OpenFile("/dev/tty", os.O_RDWR, 0)
if err != nil {
return nil, err
}
r = tty

r содержит, схематически, пару (значение, тип) --> (tty, *os.File). Обратите внимание, что тип *os.File реализует методы, отличные от Read(); даже если значение интерфейса обеспечивает доступ только к методу Read(), значение внутри несет всю информацию о типе этого значения. Вот почему мы можем делать такие вещи:

var w io.Writer
w = r.(io.Writer)

Выражение в этом присваивании является утверждением типа; оно утверждает, что элемент внутри r также реализует io.Writer, и поэтому мы можем назначить его w. После назначения w будет содержать пару (tty, *os.File). Это та же пара, что и в r. Статический тип интерфейса определяет, какие методы могут быть вызваны у интерфейсной переменной, хотя конкретное значение внутри может иметь более широкий набор методов.

Продолжая, мы можем сделать следующее:

var empty interface{}
empty = w

и пустое значение пустого поля снова будет содержать ту же пару (tty, *os.File). Это удобно: пустой интерфейс может содержать любое значение и всю информацию, которая нам когда-либо понадобится от него.

Нам здесь не нужно утверждение типа, потому что известно, что w удовлетворяет пустому интерфейсу. В примере, где мы перенесли значение из Reader в Writer, нам нужно было явно использовать утверждение типа, потому что методы Writer'а не являтся подмножеством Reader'а. Попытка преобразовать значение, которое не соответствует интерфейсу, вызовет панику.

Одна важная деталь заключается в том, что пара внутри интерфейса всегда имеет форму (значение, конкретный тип) и не может иметь форму (значение, интерфейс). Интерфейсы не поддерживают интерфейсы как значения.

Теперь мы готовы изучить reflect.

Первый закон отражения reflect

Reflection распространяется от интерфейса до reflection объекта.

На базовом уровне reflect является всего лишь механизмом для изучения пары тип и значение, хранящейся внутри переменной интерфейса. Чтобы начать работу, есть два типа, о которых нам нужно знать: reflect.Type и reflect.Value. Эти два типа предоставляют доступ к содержимому интерфейсной переменной и возвращаются простыми функциями, reflect.TypeOf() и reflect.ValueOf() соответственно. Они выделяют части из значения интерфейса. (Кроме того, из reflect.Value легко получить reflect.Type, но давайте не будем смешивать концепции Value и Type на данный момент.)

Начнем с TypeOf():

package main
import (
"fmt"
"reflect"
)

func main() {
var x float64 = 3.4
fmt.Println("type:", reflect.TypeOf(x))
}

Программа выведет

type: float64

Программа похожа на передачу простой переменной float64 x в reflect.TypeOf(). Вы видете интерфейс? А он есть — reflect.TypeOf() принимает пустой интерфейс, согласно объявлению функции:

// TypeOf() возвращает reflect.Type переменной в пустой интерфейс.
func TypeOf(i interface{}) Type

Когда мы вызываем reflect.TypeOf(x), x сначала сохраняется в пустом интерфейсе, который затем передается в качестве аргумента; reflect.TypeOf() распаковывает этот пустой интерфейс для восстановления информации о типе.

Функция reflect.ValueOf(), конечно же, восстанавливает значение (далее мы будем игнорировать шаблон и сосредоточимся на коде):

var x float64 = 3.4
fmt.Println("value:", reflect.ValueOf(x).String())

напечатает

value: <float64 Value>

(Мы вызываем метод String() явно, потому что по умолчанию пакет fmt распаковывает в reflect.Value и выводит конкретное значение.)

И reflect.Type, и reflect.Value имеют много методов, что позволяет исследовать и изменять их. Одним из важных примеров является то, что reflect.Value имеет метод Type(), который возвращает тип значения. reflect.Type и reflect.Value имеют метод Kind(), который возвращает константу, указывающую, какой примитивный элемент хранится: Uint, Float64, Slice… Эти константы объявлены в перечислении в пакете reflect. Методы Value с такими именами, как Int() и Float(), позволяют нам вытащить значения (как int64 и float64), заключённые внутри:

var x float64 = 3.4
v := reflect.ValueOf(x)
fmt.Println("type:", v.Type())
fmt.Println("kind is float64:", v.Kind() == reflect.Float64)
fmt.Println("value:", v.Float())

напечатает

type: float64
kind is float64: true
value: 3.4

Существуют также методы, такие как SetInt() и SetFloat(), но для их использования нам необходимо понять устанавливаемость (settability), тему третьего закона отражения.

Библиотека reflect имеет пару свойств, которые нужно выделить. Во-первых, чтобы API был прост, «getter» и «setter» методы Value действуют на самый большой тип, который может содержать значение: int64 для всех целых чисел со знаком. То есть метод Int() значения Value возвращает int64, а значение SetInt() принимает int64; может потребоваться преобразование в фактический тип:

var x uint8 = 'x'
v := reflect.ValueOf(x)
fmt.Println("type:", v.Type())
fmt.Println("kind is uint8: ", v.Kind() == reflect.Uint8)
x = uint8(v.Uint()) // v.Uint вернёт uint64.

будет

type: uint8
kind is uint8: true

Здесь v.Uint() вернёт uint64, необходимо явное утверждение типа.

Второе свойство состоит в том, что Kind() reflect объекта описывает базовый тип, а не статический тип. Если объект отражения содержит значение определяемого пользователем целочисленного типа, как в

type MyInt int
var x MyInt = 7
v := reflect.ValueOf(x) // v имеет тип Value.

v.Kind() == reflect.Int, хотя статический тип x является MyInt, а не int. Другими словами, Kind() не может различать int из MyInt, в отличае от Type(). Kind может принимать только значения встроенных типов.

Второй закон отражения reflect

Reflection распространяется от reflect объекта до интерфейса.

Как и физическое отражение, reflect в Go создаёт свою противоположность.

Имея reflect.Value, мы можем восстановить значение интерфейса с помощью метода Interface(); метод упаковывает информацию о типе и значении обратно в интерфейс и возвращает результат:

// Interface вернёт значение v как interface{}.
func (v Value) Interface() interface{}
bvt

Как пример:

y := v.Interface().(float64) // y имеет тип float64.
fmt.Println(y)

напечатает значение float64, представленного reflect объектом v.

Однако мы можем сделать еще лучше. Аргументы в fmt.Println() и fmt.Printf() передаются как пустые интерфейсы, которые затем распаковываются пакетом fmt внутри, как и в предыдущих примерах. Поэтому все, что требуется для печати содержимого reflect.Value правильно — передать результат метода Interface() в функцию отформатированного вывода:

fmt.Println(v.Interface())

(Почему бы не fmt.Println(v)? Потому что v имеет тип reflect.Value; мы же хотим получить содержащееся внутри значение.) Поскольку наше значение — float64, мы можем даже использовать формат с плавающей запятой, если хотим:

fmt.Printf("value is %7.1e\n", v.Interface())

выведет в конкретном случае

3.4e+00

Опять же, нет необходимости приводить тип результата v.Interface() в float64; пустое значение интерфейса содержит информацию о конкретном значении внутри, а fmt.Printf() восстановит его.

Короче говоря, метод Interface() является инверсией функции ValueOf(), за исключением того, что его результат всегда имеет статический тип interface{}.

Повторим: Reflection распространяется от значений интерфейса к объектам reflection и обратно.

Третий закон отражения reflection

Чтобы изменить объект отражения, значение должно быть устанавливаемым.

Третий закон является самым тонким и запутанным. Начинаем с первых принципов.

Вот такой код не работает, но заслуживает внимания.

var x float64 = 3.4
v := reflect.ValueOf(x)
v.SetFloat(7.1) // Ошибка

Если вы запустите этот код, он упадёт с panic с критическим сообщением:

panic: reflect.Value.SetFloat использует неадресуемое значение

Проблема не в том, что литерал 7.1 не адресуется; это то, что v не устанавливаемо. Устанавливаемость — свойство reflect.Value, и не каждое reflect.Value имеет его.

Метод reflect.Value.CanSet() сообщает о устанавливаемости Value; в нашем случае:

var x float64 = 3.4
v := reflect.ValueOf(x)
fmt.Println("settability of v:", v.CanSet())

напечатает:

settability of v: false

Ошибка вызова метода Set() на неустанавливаемом значении. Но что такое устанавливаемость?

Устанавливаемость немного напоминает адресуемость, но строже. Это свойство, при котором reflection объект может изменить хранимое значение, которое было использовано при создании reflection объекта. Устанавливаемость определяется тем, содержит ли reflection объект исходный элемент, или только его копию. Когда мы пишем:

var x float64 = 3.4
v := reflect.ValueOf(x)

мы передаем копию x в reflect.ValueOf(), поэтому интерфейс создается как аргумент для reflect.ValueOf() — это копия x, а не сам x. Таким образом, если бы утверждение:

v.SetFloat(7.1)

было бы выполнено, оно бы не обновило x, хотя v выглядит так, как будто оно было создано из x. Вместо этого он обновил бы копию x, хранящуюся внутри значения v, a сам x не был бы затронут. Это запрещено, что бы не порождать проблем, а устанавливаемость — свойство, используемое для предотвращения проблемы.

Это не должно казаться странным. Это обычная ситуация в необычной одежде. Подумайте о передаче x в функцию:

f(x)

Мы не ожидаем, что f() сможет изменить x, потому что мы передали копию значения x, а не сам x. Если мы хотим, чтобы f() непосредственно меняла x, мы должны передать нашей функции указатель на x:

f(&x)

Это прямолинейно и знакомо, и reflection работает сходно. Если мы хотим изменить x с помощью reflection, мы должны предоставить библиотеке reflection указатель на значение, которое мы хотим изменить.

Давайте сделаем это. Сначала мы инициализируем x как обычно, а затем создаем reflect.Value p, которое указывает на него.

var x float64 = 3.4
p := reflect.ValueOf(&x) // Берём адрес x.
fmt.Println("type of p:", p.Type())
fmt.Println("settability of p:", p.CanSet())

выведет

type of p: *float64

settability of p: false

Reflection объект p не может быть установлен, но это не p, который мы хотим установить, это указатель *p. Чтобы получить то, на что указывает p, мы вызываем метод Value.Elem(), который берёт значение косвенным образом через указатель, и сохраняем результат в reflect.Value v:

v := p.Elem()
fmt.Println("settability of v:", v.CanSet())

Теперь v является устанавливаемым объектом, резутьтат:

settability of v: true

и поскольку он представляет x, мы, наконец, можем использовать v.SetFloat() для изменения значения x:

v.SetFloat(7.1)
fmt.Println(v.Interface())
fmt.Println(x)

вывод, как ожидалось

7.1

7.1

Reflect может быть трудно понять, но он делает именно то, что делает язык, хотя и с помощью reflect.Type и reflection.Value, которые могут скрывать, что происходит. Просто имейте в виду, что reflection.Value нужен адрес переменной, чтобы изменить её.

Структуры

В нашем предыдущем примере v не был указателем, он был просто получен из него. Общим способом возникновения этой ситуации является использование reflection для изменения полей структуры. До тех пор, пока у нас есть адрес структуры, мы можем изменять её поля.

Вот простой пример, который анализирует значение структуры t. Мы создаем reflection объект с адресом структуры, чтобы изменять его позже. Затем устанавливаем typeOfT в его тип и итерируемся по полям, используя простые вызовы методов (см. Подробное описание пакета). Обратите внимание, что мы извлекаем имена полей из типа структуры, но сами поля являются обычными reflect.Value.

type T struct {
A int
B string
}
t := T{23, "skidoo"}
s := reflect.ValueOf(&t).Elem()
typeOfT := s.Type()
for i := 0; i < s.NumField(); i++ {
f := s.Field(i)
fmt.Printf("%d: %s %s = %v\n", i, typeOfT.Field(i).Name, f.Type(), f.Interface())
}

Программа выведет

0: A int = 23

1: B string = skidoo

Здесь проявляется еще один пункт об устанавливаемости: имена полей T в верхнем регистре (экспортируемы), потому что только экспортируемые поля устанавливаемы.

Поскольку s содержит устанавливаемый reflection объект, мы можем изменить поле структуры.

s.Field(0).SetInt(77)
s.Field(1).SetString("Sunset Strip")
fmt.Println("t is now", t)

Результат:

t is now {77 Sunset Strip}

Если мы изменим программу так, чтобы s был создан из t, а не &t, вызовы SetInt() и SetString() завершились бы паникой, поскольку поля t не были бы устанавливаемыми.

Заключение

Вспомним законы reflection:

Reflection распространяется от интерфейса до reflection объекта.

Reflection распространяется от reflection объекта до интерфейса.

Чтобы изменить reflection объект, значение должно быть устанавливаемым.

Автор Rob Pike.

Теги:

go

golang

reflect

reflection

метапрограммирование

динамическая типизация

Хабы:

Go

Программирование

+10

135

10
+10

22

Карма

0

Рейтинг

Олег Шванн @OlegSchwann

Go разработчик

Подписаться

Комментарии 10
+10

Публикации

Лучшие за суткиПохожие

A_Tsymbalyuk 13 часов назад

Физика ошибок не прощает. Она догонит тебя на крыше и заставит платить (дважды)

Простой

6 мин

8.2K

Кейс

+40

35

25
+25

BabayMazay 12 часов назад

Самодельные французские радиолампы 1920 г. Часть 3. Разрядная трубка, лампа накаливания

Средний

9 мин

1.5K

Ретроспектива

+33

9

11
+11

Bright_Translate 8 часов назад

Как я от нечего делать замедлил Postgres в 42 000 раз

Средний

11 мин

2.7K

Кейс

Перевод

+27

20

2
+2

butivskiydm 11 часов назад

Моё мини-исследование про продолжительность жизни в России

Простой

2 мин

6.4K

Мнение

+22

17

60
+60

lokosst 13 часов назад

Выделенные серверы + Kubernetes: практическое руководство

17 мин

2.4K

Туториал

+21

23

0

JetHabr 11 часов назад

«Плохие парни работают просто»: пентестеры разбирают реальные кейсы

Средний

9 мин

2.1K

Кейс

+17

19

1
+1

k0mar0v 14 часов назад

HTC Desire HD: история, дизайн и возможности легендарного смартфона

6 мин

2.6K

+17

5

5
+5

MaFrance351 13 часов назад

Домофон Лидер-М. Как скрестить трубку с ключом и всем проиграть

Простой

4 мин

2.5K

+16

5

6
+6

psygalaxy 13 часов назад

Как мы ускорили проверку документации с помощью AI-агента: от боли к решению

Средний

25 мин

1.8K

Обзор

+16

30

9
+9

kubelet 16 часов назад

DNS-хаос, зомби-поды и майнеры в кластере: самые невероятные случаи при работе с Kubernetes

Простой

14 мин

1.2K

Кейс

+16

23

1
+1

35 спикеров, аналитическая дженга и море практики: что будет на конференции для системных и бизнес-аналитиков от Контура
Турбо

Показать еще

Минуточку внимания

Турбо

Делаем сисадмина сильнее с помощью менеджера паролей

Событие

Хабр приглашает маркетологов и пиарщиков на тест-драйв

Событие

Как построить IT-бренд через вирусный контент и юмор

Вопросы и ответы

Trace/breakpoint trap (core dumped)?
LinuxПростой2 ответа

Неточность формулировки в книге или же я не понимаю?
GoПростой2 ответа

Зачем for {select} оборачивать в еще один select с time.Ticker?
GoПростой3 ответа

Какие гарантии на выравнивание адреса указателя?
GoСредний1 ответ

Как поменять env переменную PATH в golang (MacOS)?
GoСредний2 ответа

Больше вопросов на Хабр Q&A

Читают сейчас

Как превратить ноутбук в полноценное рабочее место не хуже стационарного ПК

12K

71
+71

Нашел ЛЯП в игре 5букв от Т-банка, который позволяет со 100% вероятностью угадать скрытое слово

54K

115
+115

Как выйти из IT?.. и пойти слесарем на завод. Моя попыточка дауншифтинга [4\5]

1.5K

12
+12

Моё мини-исследование про продолжительность жизни в России

6.4K

60
+60

GPT-5 получит модуль для борьбы с «галлюцинациями» — The Information

764

3
+3

35 спикеров, аналитическая дженга и море практики: что будет на конференции для системных и бизнес-аналитиков от Контура
Турбо

Истории

История Яндекс Почты

С Днём сисадмина!

Как расти в ИТ: советы, гайды и опыт сеньоров

1 055 450 очков в облаке – попробуй обогнать

Годнота из блогов компаний

Ближайшие события

7 августа

Мем-пати: как построить IT-бренд через вирусный контент и юмор

Москва

Больше событий в календаре

Маркетинг

13 августа

Как повысить эффективность врача в стоматологии на 25% с помощью ИИ?

Онлайн

Больше событий в календаре

Менеджмент

13 августа

Тест-драйв Хабра для маркетологов и пиарщиков

Москва

Больше событий в календаре

Маркетинг

16 августа

«ИТ-летник» — конференция от Совкомбанк Технологий

Казань

Больше событий в календаре

Разработка

Аналитика

3 сентября

«GoCloud Tech 2025» — IT-конференция про облака и AI

Москва • Онлайн

Больше событий в календаре

Разработка

18 – 19 сентября

Конференция по корпоративному онлайн-обучению iSpring Days 2025

Москва • Онлайн

Больше событий в календаре

Администрирование

Менеджмент

Другое

19 сентября

Премия для технологических стартапов Startech.Awards 2025

Москва • Онлайн

Больше событий в календаре

Другое

30 сентября

VII премия в сфере фармацевтического маркетинга и креатива MedMen Awards 2025

Онлайн

Больше событий в календаре

Маркетинг

Другое

30 октября

Премия «Проксима»

Москва

Больше событий в календаре

Маркетинг

Реклама

ООО ЮФО ХОСТИНГ

ИНН 5043089443

erid: 2VSb5xF8vhf

VPS/VDS с 10 Gbps

Переходите на UFO.Hosting

Ваш аккаунт

Профиль

Трекер

Диалоги

Настройки

ППА

Разделы

Статьи

Новости

Хабы

Компании

Авторы

Песочница

Информация

Устройство сайта

Для авторов

Для компаний

Документы

Соглашение

Конфиденциальность

Услуги

Корпоративный блог

Медийная реклама

Нативные проекты

Образовательные программы

Стартапам

Настройка языкаТехническая поддержка
© 2006–2025, Habr